package characters;

import ihm.views.options.ATKjOptionPane;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;


import equipments.Armor;
import equipments.Equipment;
import equipments.Items;
import equipments.enumeration.AccessoriesSlots;
import equipments.enumeration.ArmorSlots;
import equipments.enumeration.EquipmentSlots;
import equipments.enumeration.HandsSlots;

import systems.exceptions.equipments.CanNotEquipThisItem;
import systems.languages.MultiLanguages;
import systems.rpgBuilder.Classe;
import systems.rpgBuilder.Faction;
import tools.BonusAttribut;
import tools.ItemVisitor;

/**
 * Provides a class to handle a game character. A character is define by the following attributes :
 * <ul>
 *  <li>a name [String]</li>
 *  <li>a faction [Faction]</li>
 *  <li>a classe [Classe]</li>
 *  <li>a level [int]</li>
 *  <li>if he is ambidextrous [boolean]</li>
 *  <li>a mastery list [List&lt;BonusAttribut&gt;]</li>
 *  <li>an equipment [Equipment]</li>
 *  <li>a statistics</li>
 * </ul>
 * An item visitor and a private enum are also used in this class.
 * @author Baptiste Legrand & Benjamin Orman
 * @version 2.4
 */
public class Characters
{
  /**
   * Serial Version Value.
   */
  private static final long serialVersionUID = 1L;
  //Character's name
  private String name;
  //Character's faction
  private Faction faction;
  //Character's classe
  private Classe classe;
  //Character's level
  private int level;
  //Use to know if the character is ambidextrous
  private boolean isAmbidextrous;
  //Character's mastery list
  private List<BonusAttribut> masteryList;
  //Character's Equipment
  private Equipment equipment;
  //Character's statistics
  private Statistics statistics;
  //Item visitor used to visite item (not kidding!?)
  private ItemVisitor visitor;

  /**
   * This private enumaration contains all the bonus which can be found on an 
   * Item and must be added to the character.
   * @author Benjamin Orman
   * @version 2.0
   */
  private enum skillBonusAdd{
    statboostphysicaloffense,
    statboostparry,
    statboostblock,
    statboostresist,
    statboosthitaccuracy,
    statboostdodge,
    statboostmagicaldefense,
    statboostcritical,
    statboostmagicloffense,
    statboostmaxmp,
    statboostelementaldefendall,
    concentration,
    statboostphysicaldefense
  };

  /**
   * Create a character and initialized the following attributes :
   * <ul>
   *  <li>Mastery List</li>
   *  <li>Equipment</li>
   *  <li>Statistics</li>
   *  <li>ItemVisitor</li>
   * </ul>
   */
  public Characters()
  {
    this.setMasteryList(new LinkedList<BonusAttribut>());
    this.equipment   = new Equipment();
    this.equipment.setCharacter(this);
    this.statistics  = new Statistics();
    this.visitor     = new ItemVisitor(this);
    this.isAmbidextrous = false;
  }

  /**
   * Setter used to set the character's new name.
   * @param name The character's new name.
   */
  public void setName(String name) {
    this.name = name;
  }
  /**
   * Returning the character's name.
   * @return String The character's name.
   */
  public String getName() {
    return this.name;
  }
  /**
   * Set the Character's faction
   * @param newFaction The new Character's faction.
   */
  public void setFaction(Faction newFaction) {
    this.faction = newFaction;
  }
  /**
   * Returning the character's faction.
   * @return Faction The character's faction.
   */
  public Faction getFaction() {
    return faction;
  }
  /**
   * Replace the character's classe by a new one.
   * @param classe The character's new classe.
   */
  public void setClasse(Classe classe) {
    this.classe = classe;
  }
  /**
   * Return the character's classe.
   * @return Classe The character's classe.
   */
  public Classe getClasse() {
    return classe;
  }
  /**
   * Set the character's level.
   * @param level The new character's level
   */
  public void setLevel(int level) {
    this.level = level;
  }

  /**
   * Return the character's level.
   * @return int The character's level.
   */
  public int getLevel() {
    return level;
  }

  /**
   * Replace the masteryList by a new one.
   * @param masterylist The new mastery list
   */
  public void setMasteryList(List<BonusAttribut> masterylist)
  {
    this.masteryList = masterylist;
  }

  /**
   * Return the Character's mastery list.
   * @return List<BonusAttribut> The Character's mastery list.
   */
  public List<BonusAttribut> getMasteryList()
  {
    return this.masteryList;
  }

  /**
   * Initiazile the Character's stat by getting data from the Database.
   * /!\ Be sure the following attribute are set :
   * <ul>
   *  <li>classe</li>
   *  <li>level</li>
   * </ul>
   * If they are not, you will get a unknown ResultSet and the character will
   * not initialize properly.
   * @param transmission
   */
  public void initCharacterStat(Statement transmission) {
    ResultSet resultat;     
    try {
      resultat = transmission.executeQuery("Select * from "+this.classe.getTableName()+" where lvl="+this.level);
      while(resultat.next())
      {
        this.statistics.setPower(new Integer( resultat.getString("power")));
        this.statistics.setHealth(new Integer( resultat.getString("health")));
        this.statistics.setAgility(new Integer( resultat.getString("agility")));
        this.statistics.setAccuracy(new Integer( resultat.getString("accuracy")));
        this.statistics.setKnowledge(new Integer( resultat.getString("knowledge")));
        this.statistics.setWill(new Integer( resultat.getString("will")));

        this.statistics.setMaxHp(new Integer( resultat.getString("maxHp")));
        this.statistics.setMaxMp(new Integer( resultat.getString("maxMp")));
        this.statistics.setWalkSpeed(new Double( resultat.getString("walk_speed")));
        this.statistics.setRunSpeed(new Float( resultat.getString("run_speed")));
        this.statistics.setFlySpeed(new Float( resultat.getString("fly_speed")));
        this.statistics.setAttackSpeed(new Double( resultat.getString("attack_speed")));

        this.statistics.setBlock(new Integer( resultat.getString("block")));
        this.statistics.setEvasion(new Integer( resultat.getString("evasion")));
        this.statistics.setMainHandAttack(new Integer( resultat.getString("main_hand_attack")));
        this.statistics.setParry(new Integer( resultat.getString("parry")));
        this.statistics.setMainHandAccuracy(new Integer( resultat.getString("main_hand_accuracy")));
        this.statistics.setMainHandCritRate(new Integer( resultat.getString("main_hand_crit_rate")));
        this.statistics.setMagicAccuracy(new Integer( resultat.getString("magic_accuracy")));
      }
      //Init the mastery list
      this.initMastery(transmission);
    } catch (SQLException e) {
      e.printStackTrace();
    }
    this.initBonusMastery();
  }

  /**
   * Initialize the bonus mastery according to the Mastery List.
   */
  private void initBonusMastery() {

    LinkedList<String> masteryBonusList =new LinkedList<String>();
    skillBonusAdd[] listbonusValue=skillBonusAdd.values();
    for(int i=0;i<listbonusValue.length;i++)
      masteryBonusList.add(listbonusValue[i].toString());
    for(int i=0;i<this.masteryList.size();i++)
    {
      if(masteryBonusList.contains(this.masteryList.get(i).getCarac()))
      {
        switch(skillBonusAdd.valueOf(this.masteryList.get(i).getCarac()))
        {
          case statboostblock:
            this.statistics.bonusBlock= this.masteryList.get(i).getValue();
            break;
          case statboostparry:
            this.statistics.bonusParry= this.masteryList.get(i).getValue();
            break;
          case statboostdodge:
            this.statistics.bonusEvasion=this.masteryList.get(i).getValue();
            break;
          case statboostcritical:
            this.statistics.bonusMainHandCritRate=this.masteryList.get(i).getValue();
            if(this.isAmbidextrous==true){
              this.statistics.offHandCritRate=this.statistics.mainHandCritRate;
              this.statistics.bonusOffHandCritRate=this.masteryList.get(i).getValue();}
            break;
          case statboostmaxmp:
            this.statistics.bonusMp= this. masteryList.get(i).getValue();
            break;
          case statboosthitaccuracy:
            this.statistics.bonusMainHandAccuracy= this.masteryList.get(i).getValue();
            if(this.isAmbidextrous==true)
            {
              this.statistics.offHandAccuracy=this.statistics.mainHandAccuracy;
              this.statistics.bonusOffHandAccuracy=this.masteryList.get(i).getValue();}
            break;
          case statboostphysicaloffense:
            this.statistics.bonusMainHandAttack=this.masteryList.get(i).getValue();
            if(this.isAmbidextrous==true)
            {
              this.statistics.bonusOffHandAttack=this.masteryList.get(i).getValue();}
            break;
          case statboostmagicloffense:
            this.statistics.bonusMagicPower=this.masteryList.get(i).getValue();
            break;
          case statboostmagicaldefense:
            this.statistics.magicalResist=this.masteryList.get(i).getValue();
            break;
          case statboostresist:
          case statboostelementaldefendall:
            this.statistics.resistFire=this.masteryList.get(i).getValue();
            this.statistics.resistWater=this.masteryList.get(i).getValue();
            this.statistics.resistEarth=this.masteryList.get(i).getValue();
            this.statistics.resistWind=this.masteryList.get(i).getValue();
            break;
          default:break;
        }
      }

    }
  }

  /**
   * Initialize the mastery list according to the passives and actives skills
   * for a specific class and level.
   * @param statement - Statement used to communicate with the database
   */
  private void initMastery(Statement statement) {
    LinkedList<String> temp = new LinkedList<String>();//liste temporaire pour r'ecup'erer tous les skills
    LinkedList<String> SkillList = new LinkedList<String>(); //Liste finale des skills
    HashMap<String, Integer> map= new HashMap<String, Integer>();//Map pour traiter la liste temporaire et ne  garder que le level le plus haut
    ResultSet resultat;
    String[] classField = this.classe.getTableName().split("_");


    System.out.println("[Characters] initMastery debut");
    try {
      //On r'ecup`eres tous les skills d'un perso en fonction de son level et de sa classe
      resultat= statement.executeQuery("SELECT skill FROM `learn` WHERE class like '"+classField[0]+"' and pc_level <="+this.level+" and skill like 'P%'");

      while(resultat.next())
        temp.add(resultat.getString("skill"));
      //On va r'ecup`erer dans val le niveau du skill
      for(int i=0;i<temp.size();i++)
      {
        String test = temp.get(i);
        String[] decoupage = test.split("_");
        int val =0;
        switch(decoupage.length)
        {
          case 3 :
            val = Integer.parseInt(decoupage[2].substring(1));
            break;
          case 4 :
            val = Integer.parseInt(decoupage[3].substring(1));
            break;
          default : break;
        }

        //On place chaque skill dans une map, si le niveau est plus 'elev'e que la valeur pr'esente
        // on remplace la valeur existante par la nouvelle
        for(int j=i+1;j<temp.size()+1;j++)
        {
          String teste = test.substring(0,test.length()-1);
          if(!map.containsKey(teste))
            map.put(teste, val);
          else if (map.get(teste)<val)
          {
            map.remove(teste);
            map.put(teste, val);
          }
        } 

      }
      //reconstruction String du skill au level max
      Iterator<String> ite;
      ite = map.keySet().iterator();
      while(ite.hasNext())
      {
        String cle = ite.next();
        Integer value = map.get(cle);
        SkillList.add(cle+value);
      }
      //Construction de la liste finale des skills
      for(int i=0; i<SkillList.size();i++)
      {
        //effect1_reserved2
        String[] decoupage =	SkillList.get(i).split("_");
        String name="";
        switch(decoupage.length)
        {
          case 3 :
            name = decoupage[1];
            break;
          case 4 :
            name=decoupage[2];
            break;
          default : break;
        }
        if(name.equals("Dual"))
        {
          this.setAmbidextrous(true);
          this.statistics.setOffHandAttack(this.statistics.mainHandAttack);
        }
        resultat= statement.executeQuery("SELECT effect1_reserved2 FROM `skills` WHERE name like '"+SkillList.get(i)+"'");
        while(resultat.next())
          this.getMasteryList().add(new BonusAttribut(name, resultat.getInt("effect1_reserved2")));
      }
    } catch (SQLException e) {
      e.printStackTrace();
    }
    System.out.println("[Characters] initMastery fin");
  }

  /**
   * Display all the character's informations properly.
   */
  public String toString()
  {
    return "Name: "+this.name+
    "\nClass: "+this.classe+
    "\nlevel: "+this.level+
    "\nrace: "+this.faction+
    "\npower: "+this.statistics.power+
    "\nhealth: "+this.statistics.health+
    "\nagility: "+this.statistics.agility+
    "\naccuracy: "+this.statistics.accuracy+
    "\nknowledge: "+this.statistics.knowledge+
    "\nwill: "+this.statistics.will+
    "\nMaxHp: "+this.statistics.maxHp+ " bonus hp: "+this.statistics.bonusHp+
    "\nMaxMp: "+this.statistics.maxMp+ " bonus mp: "+this.statistics.bonusMp+
    "\nWalk speed: "+this.statistics.walkSpeed+
    "\nRun speed: "+this.statistics.runSpeed+
    "\nFly speed: "+this.statistics.flySpeed+
    "\nAttack speed: "+this.statistics.attackSpeed+ " bonus atk spd: "+this.statistics.bonusAttackSpeed+
    "\nEvasion: "+this.statistics.evasion+ " bonus Eva: "+this.statistics.bonusEvasion+
    "\nBlock: "+this.statistics.block+ " bonus block: "+this.statistics.bonusBlock+
    "\nParry: "+this.statistics.parry+ " bonus parry: "+this.statistics.bonusParry+
    "\nMain hand attack: "+this.statistics.mainHandAttack+ " bonus attack: "+this.statistics.bonusMainHandAttack+
    "\nMain hand accuracy: "+this.statistics.mainHandAccuracy+ " bonus accuracy: "+this.statistics.bonusMainHandAccuracy+
    "\nMain hand crit rate: "+this.statistics.mainHandCritRate+ " bonus crit rate: "+this.statistics.bonusMainHandCritRate+
    "\nMagic accuracy: "+this.statistics.magicAccuracy +" bonus magic accu: "+this.statistics.bonusMagicAccuracy+
    "\nMagic power: "+this.statistics.magicPower +" bonus magic power: "+this.statistics.bonusMagicPower;
  }

  /**
   * Setter used to replace the old equipment by a new one.
   * One you set a new equipment the statistics are update automatically.
   * @param newEquipment The new character's equipment.
   */
  public void setEquipment(Equipment newEquipment)
  {
    //Set the character's equipment
    this.equipment = newEquipment;
    //Update the statistics.
    this.updateStat();
  }

  /**
   * Update the Character's statistics according to his equipment.
   */
  private void updateStat()
  {
    //TODO : Instance of ... to change
    for(ArmorSlots slot : ArmorSlots.values())
      this.updateStat(this.equipment.getItem(slot));
    for(AccessoriesSlots slot : AccessoriesSlots.values())
      this.updateStat(this.equipment.getItem(slot));
    for(HandsSlots slot : HandsSlots.values())
      this.updateStat(this.equipment.getItem(slot));
  }

  /**
   * Updates the Character's equipment according to Item and the Slot given in
   * parameters.
   * @param slot The slot where to equip the Item
   * @param item The new item to equip.
   * @throws CanNotEquipThisItem This error is throw if you can not equip the
   *  Item in the selected slot.
   */
  public void updateEquipment(EquipmentSlots slot, Items item)
  throws CanNotEquipThisItem
  {
    Object[] values = canEquipThisItem(item);
    boolean canEquip = (Boolean) values[0];
    String errorMsg = (String) values[1];
    //First we check if the character can equip this item 
    if(canEquip)
    {
      // ... Now we can equip the item into the require slot
      if(slot instanceof ArmorSlots)
        this.equipment.updateArmor((ArmorSlots)slot, (Armor)item);
      else if(slot instanceof AccessoriesSlots)
        this.equipment.updateAccessory((AccessoriesSlots)slot, item);
      else if(slot instanceof HandsSlots)
        this.equipment.updateHand((HandsSlots)slot, item, this);    
      this.updateStat(item); 
    }else{
      throw new CanNotEquipThisItem(errorMsg);
    }
  }

  /**
   * This method is used to know if the system allows you to equip the item on
   * this Characters.
   * @param item Item to check.
   * @param errorMessage The error message generated if the function returns
   * false.
   * @return True if you can equip this item, else False
   */
  private Object[] canEquipThisItem(Items item)
  {
    Object[] values = new Object[2];
    boolean canEquip = true;
    String errorMessage = MultiLanguages.getProperties("exception.CanNotEquipThisItem");
    //First test : Is the character's level < level of item ?
    // Yes : !canEquip, else canEquip;
    if(this.level<item.getLevel())
    {
      canEquip = false;
      String tmp = MultiLanguages.getProperties("exception.CanNotEquipThisItem.level");
      errorMessage += tmp.replaceAll(" \\$1 ", String.valueOf(this.level)).
      replaceAll(" \\$2 ", String.valueOf(item.getLevel()));
    }
    //TODO complete with the mastery information

    //
    errorMessage = errorMessage.replaceAll("\\$n ", "\n").replaceAll(" \\$t ", "\t");
    //
    values[0] = canEquip;
    values[1] = errorMessage;
    return values;
  }
  /**
   * Update the Character Statistics according to a given item.
   * @param item The new item attached to the Character. It could be Armor,
   * Weapons or any other parts of the equipments.
   */
  public void updateStat(Items item)
  {
    if(item == null)
    {
      //RFU (cf : unequip)
    }else{
      item.accept(visitor);
    }
  }

  /**
   * Define if the character is ambidextrous or not.
   * @param isAmbidextrous If the character is ambidextrous or not.
   */
  public void setAmbidextrous(boolean isAmbidextrous) {
    this.isAmbidextrous = isAmbidextrous;
  }

  /**
   * Return boolean value indicating if the character is ambidextrous or not.
   * @return bool True if the character is ambidextrous, else false
   */
  public boolean isAmbidextrous() {
    return isAmbidextrous;
  }

  /**
   * Return the charachter's statistics
   * @return Statistics The character's statistics.
   */
  public Statistics getStat()
  {
    return this.statistics;
  }

  /***********************************************
   *              Statistics Getter              *
   ***********************************************/
  /**
   * @return the maxHp
   */
  public int getMaxHp() {
    return this.statistics.maxHp;
  }
  /**
   * @return the bonusHp
   */
  public int getBonusHp() {
    return this.statistics.bonusHp;
  }
  /**
   * @return the maxMp
   */
  public int getMaxMp() {
    return this.statistics.maxMp;
  }
  /**
   * @return the bonusMp
   */
  public int getBonusMp() {
    return this.statistics.bonusMp;
  }
  /**
   * @return the power
   */
  public int getPower() {
    return this.statistics.power;
  }
  /**
   * @return the health
   */
  public int getHealth() {
    return this.statistics.health;
  }
  /**
   * @return the agility
   */
  public int getAgility() {
    return this.statistics.agility;
  }
  /**
   * @return the accuracy
   */
  public int getAccuracy() {
    return this.statistics.accuracy;
  }
  /**
   * @return the knowledge
   */
  public int getKnowledge() {
    return this.statistics.knowledge;
  }
  /**
   * @return the will
   */
  public int getWill() {
    return this.statistics.will;
  }
  /**
   * @return the walkSpeed
   */
  public double getWalkSpeed() {
    return this.statistics.walkSpeed;
  }
  /**
   * @return the runSpeed
   */
  public float getRunSpeed() {
    return this.statistics.runSpeed;
  }
  /**
   * @return the flySpeed
   */
  public float getFlySpeed() {
    return this.statistics.flySpeed;
  }
  /**
   * @return the attackSpeed
   */
  public double getAttackSpeed() {
    return this.statistics.attackSpeed;
  }
  /**
   * @return the bonusAttackSpeed
   */
  public double getBonusAttackSpeed() {
    return this.statistics.bonusAttackSpeed;
  }
  /**
   * @return the evasion
   */
  public int getEvasion() {
    return this.statistics.evasion;
  }
  /**
   * @return the bonusEvasion
   */
  public int getBonusEvasion() {
    return this.statistics.bonusEvasion;
  }
  /**
   * @return the block
   */
  public int getBlock() {
    return this.statistics.block;
  }
  /**
   * @return the bonusBlock
   */
  public int getBonusBlock() {
    return this.statistics.bonusBlock;
  }
  /**
   * @return the parry
   */
  public int getParry() {
    return this.statistics.parry;
  }
  /**
   * @return the bonusParry
   */
  public int getBonusParry() {
    return this.statistics.bonusParry;
  }
  /**
   * @return the mainHandAttack
   */
  public int getMainHandAttack() {
    return this.statistics.mainHandAttack;
  }
  /**
   * @return the bonusHandAttack
   */
  public int getBonusMainHandAttack() {
    return this.statistics.bonusMainHandAttack;
  }
  /**
   * @return the mainHandAccuracy
   */
  public int getMainHandAccuracy() {
    return this.statistics.mainHandAccuracy;
  }
  /**
   * @return the bonusMainHandAccuracy
   */
  public int getBonusMainHandAccuracy() {
    return this.statistics.bonusMainHandAccuracy;
  }
  /**
   * @return the mainHandCritRate
   */
  public int getMainHandCritRate() {
    return this.statistics.mainHandCritRate;
  }
  /**
   * @return the bonusMainHandCritRate
   */
  public int getBonusMainHandCritRate() {
    return this.statistics.bonusMainHandCritRate;
  }

  /**
   * @return the offHandAttack
   */
  public int getOffHandAttack() {
    return this.statistics.offHandAttack;
  }
  /**
   * @return the bonusOffHandAttack
   */
  public int getBonusOffHandAttack() {
    return this.statistics.bonusOffHandAttack;
  }
  /**
   * @return the offHandAccuracy
   */
  public int getOffHandAccuracy() {
    return this.statistics.offHandAccuracy;
  }
  /**
   * @return the bonusMainHandAccuracy
   */
  public int getBonusOffHandAccuracy() {
    return this.statistics.bonusOffHandAccuracy;
  }
  /**
   * @return the mainHandCritRate
   */
  public int getoffHandCritRate() {
    return this.statistics.offHandCritRate;
  }
  /**
   * @return the bonusOffHandCritRate
   */
  public int getBonusOffHandCritRate() {
    return this.statistics.bonusOffHandCritRate;
  }

  /**
   * @return the magicAccuracy
   */
  public int getMagicAccuracy() {
    return this.statistics.magicAccuracy;
  }
  /**
   * @return the bonusMagicAccuracy
   */
  public int getBonusMagicAccuracy() {
    return this.statistics.bonusMagicAccuracy;
  }
  /**
   * @return the magicPower
   */
  public int getMagicPower() {
    return this.statistics.magicPower;
  }
  /**
   * @return the bonusMagicPower
   */
  public int getBonusMagicPower() {
    return this.statistics.bonusMagicPower;
  }
  /**
   * @return the magicalResist
   */
  public int getMagicalResist() {
    return this.statistics.magicalResist;
  }
  /**
   * @return the bonusMagicResist
   */
  public int getBonusMagicResist() {
    return this.statistics.bonusMagicResist;
  }
  /**
   * @return the physicalDef
   */
  public int getPhysicalDef() {
    return this.statistics.physicalDef;
  }
  /**
   * @return the bonusPhyDef
   */
  public int getBonusPhyDef() {
    return this.statistics.bonusPhyDef;
  }
  /**
   * @return the resistFire
   */
  public int getResistFire() {
    return this.statistics.resistFire;
  }
  /**
   * @return the resistWater
   */
  public int getResistWater() {
    return this.statistics.resistWater;
  }
  /**
   * @return the resistWind
   */
  public int getResistWind() {
    return this.statistics.resistWind;
  }
  /**
   * @return the resistEarth
   */
  public int getResistEarth() {
    return this.statistics.resistEarth;
  }

  /**
   * This function returns the current equipments assign to the character.
   * @return the current equipments.
   */
  public Equipment getEquipment(){
    return this.equipment;
  }
}